/**
 * Copyright 2004 Cordys R&D B.V. 
 * 
 * This file is part of the Cordys FTP Connector. 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 package com.eibus.applicationconnector.ftp;

import java.io.IOException;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Vector;

import com.eibus.xml.nom.Document;
import com.eibus.xml.nom.Node;
import com.eibus.util.logger.Severity;
import com.eibus.util.logger.CordysLogger;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Proxy;
import com.jcraft.jsch.ProxyHTTP;
import com.jcraft.jsch.ProxySOCKS4;
import com.jcraft.jsch.ProxySOCKS5;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.ChannelSftp.LsEntry;

/**
 * This class implements IFTPClient by abstracting
 * connection to Netcraft's FTP Client. Implements
 * the common commands of FTP.  
 */
public class CordysSFTPClient implements IFTPClient
{
	public ChannelSftp ftpClient;
	private static Document document = null;
	private static final CordysLogger logger = CordysLogger.getCordysLogger(CordysSFTPClient.class);

	private static final String CREATE_DIRECTORY_FAILURE = "<dir_name>: Cannot create a file when that file already exists or the access is denied.";
	private static final String REMOVE_DIRECTORY_FAILURE = "<dir_name>: Cannot find the directory or the directory is not empty.";
	
	/**
	 * Connects to the ftp server whose connection details
	 * are available in ftpConfiguration
	 */
	@Override
	public void connect(FTPConfiguration ftpConfiguration) throws SocketException, IOException, FTPException
	{
		document = ftpConfiguration.getDocument();

		JSch jsch = new JSch();
        Session session = null;
        Channel channel = null;
        
        try
        {
            session = jsch.getSession(ftpConfiguration.getUsername(), ftpConfiguration.getServer(), ftpConfiguration.getPort());
            session.setPassword(ftpConfiguration.getPassword());

            java.util.Properties config = new java.util.Properties();
            config.put("StrictHostKeyChecking", "no");
            session.setConfig(config);
            
    		/* 
    		 * If the proxy is enabled, create the proxy object and
    		 * set it to the session object
    		 */ 
            if (ftpConfiguration.isProxyEnabled())
            {
            	Proxy proxy = getProxy(ftpConfiguration.getProxySettings());
            	session.setProxy(proxy);
            }
            
            session.connect();
            channel = session.openChannel("sftp");
            channel.connect();
            ftpClient = (ChannelSftp)channel;
        }
        catch (Exception e)
        {
        	logger.log(Severity.ERROR, "An error has occurred while connecting to SFTP server. " + e.getMessage());
        	throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
        }
	}
	
	/**
	 * APPEND command implementation
	 */
	@Override
	public String append(String sourceFile, String targetFile) throws FTPException
	{
		try
		{
			ftpClient.put(sourceFile, targetFile, ChannelSftp.APPEND);
		}
		catch (SftpException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing append command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		return "Transfer complete.\n";
	}

	/**
	 * MKDIR command implementation
	 * path: Path of the directory to be created, relative to FTP's root directory 
	 */
	@Override
	public String createDirectory(String path) throws FTPException
	{
		try
		{
			ftpClient.mkdir(path);
		}
		catch (SftpException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing MKDIR command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, CREATE_DIRECTORY_FAILURE.replace("<dir_name>", path), e));
		}
		return "\"" + path + "\"" + " directory created.\n"; 
	}

	/**
	 * DELETE command implementation
	 * path: path of the file to be deleted
	 */
	@Override
	public String delete(String path) throws FTPException
	{
		try
		{
			ftpClient.rm(path);
		}
		catch (SftpException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing DEL command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		
		return "DELE command successful.\n";
	}

	/**
	 * Multiple files delete implementation
	 * If path refers to a file, delete it
	 * If path refers to a directory, delete all the files in that directory
	 */
	@Override
	public int deleteFiles(String path) throws FTPException
	{
		int nodeToReturn = document.createElement("nodeToReturn");
		
		path = FTPCommand.getProperFileSeparator(path, true);

		String directoryName = path; 
		
		if (isDirectory(path, directoryName))
		{
			logger.log(Severity.INFO, "Given path refers to a directory.");
			Node.appendToChildren(deleteFilesInDirectory(path), nodeToReturn);
		}
		else
		{
			logger.log(Severity.INFO, "Given path refers to a file.");
			String replyString = delete(path);
			document.createTextElement("status", replyString, nodeToReturn);
		}
		return nodeToReturn;
	}

	/**
	 * Delete all the files in the given directory
	 * @param path
	 * @return
	 * @throws FTPException
	 */
	private int deleteFilesInDirectory(String path) throws FTPException
	{
		Vector<String> fileList = new Vector<String>();
		getListOfFiles(path, fileList);
		
		int filesNode = 0;

		filesNode = document.createElement("dir");
		
		for (String filePath : fileList)
		{
			int fileNode = document.createElementNS("file", filePath, filesNode);
			String replyString = delete(path + '/' + filePath);
			document.createElementNS("status", replyString, fileNode);
		}
		
		return filesNode;
	}

	/**
	 * isDirectory() returns true if the path passed to it represents a directory
	 * else, it returns false
	 * @param path
	 * @return
	 */
	private boolean isDirectory(String path, String directoryName) throws FTPException
	{
		String parentDirectory = path.substring(0, path.lastIndexOf('/'));
		
		if (parentDirectory.equals(""))
		{
			try
			{
				parentDirectory = ftpClient.getHome();
			}
			catch (SftpException e)
			{
				logger.log(Severity.ERROR, e.getMessage());
			}
		}
		Vector<LsEntry> fileList = new Vector();
		try
		{
			fileList = ftpClient.ls(parentDirectory);
		}
		catch (SftpException e)
		{
			logger.log(Severity.ERROR, e.getMessage());
		}
		
		if (fileList != null)
		{
			for (LsEntry lsEntry : fileList)
			{
				if (!(lsEntry.getFilename().equals(".") || lsEntry.getFilename().equals("..")))
				{
					if ((directoryName.equals(lsEntry.getFilename()) || directoryName.substring(directoryName.lastIndexOf("/") + 1).equals(lsEntry.getFilename())) && lsEntry.getLongname().startsWith("d"))	//"d" for directory
					{
						return true;
					}
				}
			}
		}
		return false;
	}

	
	/**
	 * Disconnect from the server and logout
	 */
	@Override
	public void disconnect()
	{
		/* Make sure the client is disconnected only if it is already connected */
		if (ftpClient != null && ftpClient.isConnected())
		{
			ftpClient.disconnect();		
			logger.log(Severity.DEBUG, "Logging out from FTP Server");
		}
	}
	
	/**
	 * GET command implementation
	 */
	@Override
	public String download(String sourceFile, String targetFile) throws FTPException
	{
		try
		{
			ftpClient.get(sourceFile, targetFile);
		}
		catch (SftpException e)
		{
			String exceptionMessage = "";
			if (e.getMessage().length() == 0)
			{
				exceptionMessage = e.getCause().toString();
			}
			else
			{
				exceptionMessage = e.getMessage();
			}
			logger.log(Severity.ERROR, "An error has occurred while downloading the file from SFTP server. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, exceptionMessage, e));
		}
		
		return "Transfer complete.\n";
	}

	/**
	 * LS command implementation
	 */
	@Override
	public String getListOfFiles(String path, List<String> fileList) throws FTPException
	{
		try
		{
			if (path.equals(""))
			{
				path = ftpClient.getHome();
			}
			Vector<LsEntry> listOfFiles = ftpClient.ls(path);
			
			for (LsEntry lsEntry : listOfFiles)
			{
				if (!(lsEntry.getFilename().equals(".") || lsEntry.getFilename().equals("..")))
				{
					fileList.add(lsEntry.getFilename());
				}
			}
	                        
		}
		catch (SftpException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing LS command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), path));
		}
		
		return "Transfer complete.\n";
	}

	@Override
	public String getListOfFilesWithDetails(String path, List<CordysFTPFile> cordysFTPFileList) throws FTPException 
	{
		try
		{
			if (path.equals(""))
			{
				path = ftpClient.getHome();
			}
			Vector<LsEntry> listOfFiles = ftpClient.ls(path);
			
			for (LsEntry lsEntry : listOfFiles)
			{
				if (!(lsEntry.getFilename().equals(".") || lsEntry.getFilename().equals("..")))
				{
					SftpATTRS attributes = lsEntry.getAttrs();
					CordysFTPFile cordysFTPFile = new CordysFTPFile();
					cordysFTPFile.setFileName(lsEntry.getFilename());
					cordysFTPFile.setDirectory(attributes.isDir());
					cordysFTPFile.setFileSize(attributes.getSize());
					
					SimpleDateFormat fromDateFormat = new SimpleDateFormat("EEE MMM d HH:mm:ss z yyyy");
					SimpleDateFormat toDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

					Date date = fromDateFormat.parse(attributes.getMtimeString());
					
					cordysFTPFile.setFileModificationDate(toDateFormat.format(date));
					cordysFTPFileList.add(cordysFTPFile);
				}
			}
	                        
		}
		catch (SftpException e)
		{
			logger.log(Severity.ERROR, "An error occurred while executing LS command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), path));
		}
		catch (java.text.ParseException e)
		{
			logger.log(Severity.ERROR, "An error occurred while parsing the date. " + e.getMessage());
		}
		
		return "Transfer complete.\n";
	}
	
	/**
	 * DIR command implementation
	 */
	@Override
	public int listDirectories(int directoryPathNode) throws FTPException
	{
		String recurssiveAttrVal = Node.getAttribute(directoryPathNode, "recursive", "false");
		String dir = Node.getDataWithDefault(directoryPathNode, "");
		dir = FTPCommand.getProperFileSeparator(dir, true);
		
		if(recurssiveAttrVal.equals("true"))
		{
			int	fileSetNode = document.createElement("listOfFiles");
			return listDirectoryRecursively(dir, fileSetNode);
		}
		else
		{
			int	listNode = document.createElement("listOfFiles");
			
			Vector<String> listOfFiles = new Vector<String>();
			String replyString = getListOfFiles(dir, listOfFiles);
			
	        for(String filename : listOfFiles)
	        {
				document.createTextElement("file", filename, listNode);
			}
	        
			Node.appendToChildren(document.createTextElement("status", replyString), listNode);
			return listNode;
		}
	}
	
	private int listDirectoryRecursively(String directoryPath, int fileSetNode) throws FTPException
	{
		Vector<LsEntry> listOfFiles = null;
		try
		{
			if (directoryPath.equals(""))
			{
				directoryPath = ftpClient.getHome();
			}
			listOfFiles = ftpClient.ls(directoryPath);
		}
		catch (SftpException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while executing listDirectory command. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), directoryPath));
		}
		
		int dirNode = document.createElement("dir", fileSetNode);
		int listNode = document.createElement("listOfFiles", dirNode);
		Node.setAttribute(dirNode, "path", directoryPath);
		
		if (listOfFiles != null)
		{
			for (LsEntry lsEntry : listOfFiles)
			{
				if (!(lsEntry.getFilename().equals(".") || lsEntry.getFilename().equals("..")))
				{
					Node.appendToChildren(document.createTextElement("file", lsEntry.getFilename()), listNode);
					
					if (lsEntry.getLongname().startsWith("d"))	//"d" for directory
					{
						String dirPath=lsEntry.getFilename();
						if (!directoryPath.equals(""))
						{
							dirPath = directoryPath + "/" + lsEntry.getFilename();
						}
						listDirectoryRecursively(dirPath, fileSetNode);
					}
				}
			}
		}
		
		Node.appendToChildren(document.createTextElement("status", "Transfer complete.\n"), listNode);
		return fileSetNode;
	}
	
	/**
	 * PWD command implementation
	 */
	@Override
	public String printWorkingDirectory()
	{
		String currentWorkingDirectory = "";
		try
		{
			currentWorkingDirectory = ftpClient.pwd();
		}
		catch (SftpException e)
		{
			logger.log(Severity.ERROR, "An error has occured while executing PWD command. " + e.getMessage(), e);
		}
		return currentWorkingDirectory;
	}

	/**
	 * RMDIR command implementation
	 */
	@Override
	public String removeDirectory(String directoryPath) throws FTPException
	{
		try
		{
			ftpClient.rmdir(directoryPath);
		}
		catch (SftpException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while deleting the directory. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, REMOVE_DIRECTORY_FAILURE.replace("<dir_name>", directoryPath), e));
		}
		
		return "RMD command successful.\n";
	}

	
	/**
	 * RENAME command implementation
	 */
	@Override
	public String rename(String from, String to) throws FTPException
	{
		try
		{
			ftpClient.rename(from, to);
		}
		catch (SftpException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while renaming the file. " + e.getMessage());
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));
		}
		
		return "RNTO command successful.\n";
	}

	@Override
	public void setFileType(FILE_TYPE filetype)
	{
		// SFTP doesn't support specifying the transfer type
	}

	/**
	 * PUT command implementation
	 */
	@Override
	public String upload(String sourceFile, String targetFile) throws FTPException
	{
		try
		{
			ftpClient.put(sourceFile, targetFile);
		} catch (SftpException e)
		{
			logger.log(Severity.ERROR, "An error has occurred while uploading the file. " + e);
			throw (new FTPException(FTPHandler.serverFaultCode, e.getMessage(), e));	
		}
		return "Transfer complete.\n";
	}
	
    /**
     * This method is used to get the proxy object depending on the type of 
     * proxy: HTTP, SOCKS4 or SOCKS5
     *
     * @return Proxy
     */
    private Proxy getProxy(ProxySettings proxySettings)
    {
        Proxy proxy = null;
        
        if (proxySettings.getProxyType().equals("HTTP"))
        {
        	proxy = new ProxyHTTP(proxySettings.getProxyHost(), proxySettings.getProxyPort());
        	if ((proxySettings.getProxyUsername() != null) && (proxySettings.getProxyUsername().length() != 0))
        	{
        		((ProxyHTTP)proxy).setUserPasswd(proxySettings.getProxyUsername(), proxySettings.getProxyPassword());        		
        	}
        }
        if (proxySettings.getProxyType().equals("SOCKS4"))
        {
        	proxy = new ProxySOCKS4(proxySettings.getProxyHost(), proxySettings.getProxyPort());
        	if ((proxySettings.getProxyUsername() != null) && (proxySettings.getProxyUsername().length() != 0))
        	{
        		((ProxySOCKS4)proxy).setUserPasswd(proxySettings.getProxyUsername(), proxySettings.getProxyPassword());        		
        	}
        }
        if (proxySettings.getProxyType().equals("SOCKS5"))
        {
        	proxy = new ProxySOCKS5(proxySettings.getProxyHost(), proxySettings.getProxyPort());
        	((ProxySOCKS5)proxy).setUserPasswd(proxySettings.getProxyUsername(), proxySettings.getProxyPassword());        		
        }
        
        return proxy;
    }
}